# Schrenckii Pixmap Loader
# ------------------------
#
# Defines the PixmapLoader class which contains the pixmaps for each tile
# colour drawn at each possible tile location over the background, as well as
# the methods to do so.
#
# Author: Stephen D'Angelo (mallow40 [at] gmail)

import cairo
import gtk
import pango
import rsvg

import data
import game

class PixmapLoader:
    
    def __init__(self, theme, drawable, gc):
        self.theme = theme 

        # Load svg files for the various coloured tiles
        self.svgs = {}
        self.tile_pixmaps = {}

        # drawable is used to get the depth value when creating new pixmaps
        self.drawable = drawable

        # gc is used when drawing to pixmaps
        self.gc = gc

        self.anims = []

        self.load_svg()

    
    def load_svg(self):
        for colour in data.colours:
            filename = "./" + self.theme + "/" + colour + ".svg"
            self.svgs[colour] = rsvg.Handle(file=filename)

        filename = "./" + self.theme + "/bg.svg"
        self.bg_svg = rsvg.Handle(file=filename)

        filename = "./" + self.theme + "/nh.svg"
        self.nh_bg_svg = rsvg.Handle(file=filename)


    def new_theme(self):
        newtheme = self.gs.settings.value("theme/theme")
        if newtheme != self.theme:
            self.theme = newtheme
            self.load_svg()
            self.reload_pixmaps(self.width, self.height)


    def reload_pixmaps(self, width, height):
        # From the new width and height, calculate the size of a tile
        self.width = width
        self.height = height

        # (+5) because the next and hold piece areas are beside the game board
        tiles_across = data.board_width + 5

        self.psize = int(height/data.board_height)
        
        if self.psize * tiles_across > width:
            self.psize = int(width/tiles_across)

        # Font size for basic drawing
        self.fontsize = self.psize / 3

        # Font colour
        self.fontcolour = (0.5, 0.5, 0.5, 1.0)

        # Recreate Pixmaps to draw to
        self.board_width = self.psize * data.board_width
        self.board_height = self.psize * data.board_height

        self.nhsize = self.psize * 5

        # Calculate top left x, y for game to be located at
        self.game_x = int((width - (self.board_width + self.nhsize)) / 2)
        self.game_y = int((height - (self.board_height)) / 2)
        
        # board_pixmap is the current state of the game board (pieces that
        # have already fallen to the bottom)
        self.board_pixmap = gtk.gdk.Pixmap(self.drawable, self.board_width,
            self.board_height)
        black_out_pixmap(self.board_pixmap)

        # game_pixmap is the composition of board_pixmap and the current
        # in-play piece
        self.game_pixmap = gtk.gdk.Pixmap(self.drawable,
            self.board_width, self.board_height)
        black_out_pixmap(self.game_pixmap)

        # state_pixmap is the composition of board_pixmap
        # the next and hold pieces, and the score. Sections of this pixmap are
        # drawn to the window based on expose events.
        self.state_pixmap = gtk.gdk.Pixmap(self.drawable, width, height)
        black_out_pixmap(self.state_pixmap)

        # Reload background pixmap
        print "Recreating background Pixmap"

        self.bg_pixmap = gtk.gdk.Pixmap(self.drawable, self.board_width,
            self.board_height)

        black_out_pixmap(self.bg_pixmap)

        bgw = self.bg_svg.get_pixbuf().get_width()
        bgh = self.bg_svg.get_pixbuf().get_height()

        context = self.bg_pixmap.cairo_create()
        context.scale(float(self.board_width) / bgw,
            float(self.board_height) / bgh)

        self.bg_svg.render_cairo(context)

        # Reload tile pixmaps for each colour
        self.tile_pixmaps = {}
        self.resized_pixbufs = {}

        for colour in data.colours:
            #print "Recreating Pixmap for " + str(colour)

            pixbuf = self.svgs[colour].get_pixbuf()

            self.tile_pixmaps[colour] = {}

            # Create a pixmap (copied from the background) for each colour
            # and tile each colour's block across it
            #self.tile_pixmaps[colour] = gtk.gdk.Pixmap(self.drawable,
            #    self.board_width, self.board_height)

            #self.tile_pixmaps[colour].draw_drawable(self.gc, self.bg_pixmap,
            #    0, 0, 0, 0, -1, -1)

            # Create pixbuf to tile over the background (to avoid x*y
            # slow cairo resizes)
            self.resized_pixbufs[colour] = pixbuf.scale_simple(self.psize,
                self.psize, gtk.gdk.INTERP_BILINEAR)

            # Draw the resized pixbuf for each colour over this large
            # pixmap at each x, y block co-ordinate
            for x in range(0, data.board_width):

                self.tile_pixmaps[colour][x] = {}

                for y in range(0, data.board_height):
                    self.tile_pixmaps[colour][x][y] = None

                    #xcoor = x * self.psize
                    #ycoor = y * self.psize
                    #self.tile_pixmaps[colour].draw_pixbuf(self.gc,
                    #    self.resized_pixbufs[colour], 0, 0, xcoor, ycoor)

        # The backgrounds for the next and hold piece areas
        print "Recreating Next Pixmap"
        self.nh_bg_pixmap = gtk.gdk.Pixmap(self.drawable, self.nhsize,
                self.nhsize)

        black_out_pixmap(self.nh_bg_pixmap)

        nhw = self.nh_bg_svg.get_pixbuf().get_width()
        nhh = self.nh_bg_svg.get_pixbuf().get_height()

        context = self.nh_bg_pixmap.cairo_create()
        context.scale(float(self.nhsize) / nhw, float(self.nhsize) / nhh)

        self.nh_bg_svg.render_cairo(context)

        # nh_pixmaps is a dictionary from kind to an image of that
        # piece that can be used as a view of the next piece or the hold
        # piece. Update them now.
        self.nh_pixmaps = {}

        for kind in data.kinds:
            self.nh_pixmaps[kind] = {}
            for colour in data.colours:
                self.nh_pixmaps[kind][colour] = None

        # Make a blank one for the case that there's no hold piece
        self.nh_pixmaps[None] = gtk.gdk.Pixmap(self.drawable, self.nhsize,
                self.nhsize)
        
        self.nh_pixmaps[None].draw_drawable(self.gc, self.nh_bg_pixmap, 0, 0,
            0, 0, self.nhsize, self.nhsize)

        # ntitle_pixmap holds the words "Next Piece"
        self.ntitle_pixmap = gtk.gdk.Pixmap(self.drawable, self.nhsize,
            int(self.psize/2))

        black_out_pixmap(self.ntitle_pixmap)
        draw_text("Next Piece", self.fontsize, self.fontcolour,
            self.ntitle_pixmap)

        # htitle_pixmap holds the words "Hold Piece"
        self.htitle_pixmap = gtk.gdk.Pixmap(self.drawable, self.nhsize,
            int(self.psize/2))

        black_out_pixmap(self.htitle_pixmap)
        draw_text("Hold Piece", self.fontsize, self.fontcolour,
            self.htitle_pixmap)

        # score_pixmap is the same size as the next and hold pixmaps and is
        # drawn underneath them
        self.score_pixmap = gtk.gdk.Pixmap(self.drawable, self.nhsize,
            self.nhsize)
        black_out_pixmap(self.score_pixmap)


    def draw_block(self, context, x, y, colour, alpha=None):
        """Draw a block of the given colour on the given context at the
        given x, y coordinates.  If alpha is not None, we will use cairo to
        draw a transparent "shadow" tile, otherwise, we'll just copy the
        pixmap over with draw_drawable."""

        xcoor = x * self.psize
        ycoor = y * self.psize

        if self.tile_pixmaps[colour][x][y] == None:
            #print "Pixmap: %s %s %s" % (colour, x, y)
            # Create a pixmap for this section the size of a piece
            self.tile_pixmaps[colour][x][y] = gtk.gdk.Pixmap(self.drawable,
                self.psize, self.psize)
            
            # Draw the correct section of the background to it
            self.tile_pixmaps[colour][x][y].draw_drawable(self.gc,
                self.bg_pixmap, xcoor, ycoor, 0, 0, self.psize, self.psize)

            # Draw the correct colour to it
            self.tile_pixmaps[colour][x][y].draw_pixbuf(self.gc,
                self.resized_pixbufs[colour], 0, 0, 0, 0)

        if alpha is None:
            context.draw_drawable(self.gc, self.tile_pixmaps[colour][x][y],
                0, 0, xcoor, ycoor, self.psize, self.psize)

        else:
            context.set_source_pixmap(self.tile_pixmaps[colour][x][y],
                xcoor, ycoor)
            context.rectangle(xcoor, ycoor, self.psize, self.psize)
            context.save()
            context.clip()
            context.paint_with_alpha(alpha)
            context.restore()
            
        #if alpha is None:
        #    context.draw_drawable(self.gc, self.tile_pixmaps[colour],
        #        x, y, x, y, self.psize, self.psize)

        #else:
        #    context.set_source_pixmap(self.tile_pixmaps[colour], 0, 0)
        #    context.rectangle(x, y, self.psize, self.psize)
        #    context.save()
        #    context.clip()
        #    context.paint_with_alpha(alpha)
        #    context.restore()


    def draw_board_pixmap(self):
        """Redraws the board pixmap whenever the state of the board changes
        (i.e. a piece hits the bottom)."""

        # Draw the background
        self.board_pixmap.draw_drawable(self.gc, self.bg_pixmap, 0, 0, 0, 0,
            self.board_width, self.board_height)

        # Draw the board
        for i in range(0, data.board_height):

            for j in range(0, data.board_width):
                colour = self.gs.board.board[(j,i)]

                if colour != "None":
                    self.draw_block(self.board_pixmap, j, i, colour)

                    #i2 = i * self.psize
                    #j2 = j * self.psize
                    #self.draw_block(self.board_pixmap, j2, i2, colour)


    def draw_game_pixmap(self):
        """Redraws the game area pixmap everytime a piece moves by copying the
        board_pixmap and drawing the current piece on top."""

        # Draw board_pixmap to game_pixmap
        self.game_pixmap.draw_drawable(self.gc, self.board_pixmap, 0, 0, 0, 0,
            self.board_width, self.board_height)

        # If its a game over, don't draw the current piece
        if not self.gs.gameover:
            piece = self.gs.piece
            shadow = self.gs.hard_drop(0)
            colour = piece._colour
            
            # If the settings say so, draw the shadow
            if self.gs.settings.value("gameplay/shadow"):
                for (x,y) in shadow._coor:
                    self.draw_block(self.game_pixmap.cairo_create(), x, y,
                        colour, 0.3)

                    #x = x * self.psize
                    #y = y * self.psize
                    #self.draw_block(self.game_pixmap.cairo_create(), x, y,
                    #    colour, 0.3)

            # Draw the piece
            for (x,y) in piece._coor:
                self.draw_block(self.game_pixmap, x, y, colour)

                #x = x * self.psize
                #y = y * self.psize
                #self.draw_block(self.game_pixmap, x, y, colour)

        # If it's a game over, draw "Game Over" on the screen
        if self.gs.gameover:
            self.draw_text_overlay(self.game_pixmap, "GAME OVER")

        # Else, if it's paused, draw "Paused" on the screen
        elif self.gs.paused:
            self.draw_text_overlay(self.game_pixmap, "PAUSED")


    def draw_score(self):
        black_out_pixmap(self.score_pixmap)

        string = "Level: %s\nLines: %s\nScore: %s" %\
            (self.gs.level, self.gs.lines, self.gs.score)

        draw_text(string, self.fontsize, self.fontcolour, self.score_pixmap,
            int(self.psize))


    def draw_nh_pixmap(self, piece):
        piece = game.Piece(piece)

        kind = piece._kind
        colour = piece._colour

        #print "Recreating Pixmap for " + str(kind) + " " + str(colour)

        (pwidth, pheight) = piece_size(piece)
        padleft = (self.nhsize - self.psize * pwidth)/2
        padtop = (self.nhsize - self.psize * pheight)/2
        
        self.nh_pixmaps[kind][colour] = gtk.gdk.Pixmap(self.drawable,
            self.nhsize, self.nhsize)

        black_out_pixmap(self.nh_pixmaps[kind][colour])
        
        # Draw the background
        self.nh_pixmaps[kind][colour].draw_drawable(self.gc, self.nh_bg_pixmap,
            0, 0, 0, 0, self.nhsize, self.nhsize)
    
        # Draw the piece
        for (x,y) in piece._coor:
            xcoor = x * self.psize + padleft
            ycoor = y * self.psize + padtop

            self.nh_pixmaps[kind][colour].draw_pixbuf(self.gc,
                self.resized_pixbufs[colour], 0, 0, xcoor, ycoor)


    def refresh_state(self, area):
        """Recopies the pixmap "area" to the state_pixmap.
        This way, only the needed pixmaps are recopied."""

        if area == "game":
            self.state_pixmap.draw_drawable(self.gc, self.game_pixmap, 0, 0,
                self.game_x, self.game_y, self.board_width, self.board_height)

            # Draw animations
            for anim in self.anims:
                anim.render(self.state_pixmap, self.game_y + self.board_height, 
                    self.game_x)

        elif area == "next":
            kind = self.gs.nextpiece._kind
            colour = self.gs.nextpiece._colour
            next_pixmap = self.nh_pixmaps[kind][colour]

            if next_pixmap is None:
                self.draw_nh_pixmap(self.gs.nextpiece)
                next_pixmap = self.nh_pixmaps[kind][colour]

            (w, h) = self.ntitle_pixmap.get_size()

            y = self.game_y + h

            self.state_pixmap.draw_drawable(self.gc, next_pixmap, 0, 0,
                self.game_x + self.board_width, y, self.nhsize, self.nhsize)

            # The only time the score changes is when a new pieces comes
            self.draw_score()

            y = self.game_y + 2 * self.nhsize + 4 * h

            self.state_pixmap.draw_drawable(self.gc, self.score_pixmap, 0, 0,
                self.game_x + self.board_width, y, self.nhsize, self.nhsize)

        elif area == "hold":
            if self.gs.holdpiece is not None:
                kind = self.gs.holdpiece._kind
                colour = self.gs.holdpiece._colour
                hold_pixmap = self.nh_pixmaps[kind][colour]

                if hold_pixmap is None:
                    self.draw_nh_pixmap(self.gs.holdpiece)
                    hold_pixmap = self.nh_pixmaps[kind][colour]

            else:
                hold_pixmap = self.nh_pixmaps[None]

            (w, h) = self.ntitle_pixmap.get_size()

            y = self.game_y + self.nhsize + 3 * h

            self.state_pixmap.draw_drawable(self.gc, hold_pixmap, 0, 0,
                self.game_x + self.board_width, y, self.nhsize, self.nhsize)

        elif area == "titles":
            (w, h) = self.ntitle_pixmap.get_size()

            x = self.game_x + self.board_width

            y = self.game_y
            self.state_pixmap.draw_drawable(self.gc, self.ntitle_pixmap, 0, 0,
                x, y, w, h) 

            y = self.game_y + self.nhsize + 2 * h
            self.state_pixmap.draw_drawable(self.gc, self.htitle_pixmap, 0, 0,
                x, y, w, h) 


    def draw_text_overlay(self, pixmap, string):
        """This is too slow at the moment.
        # Desaturate the underlying pixmap
        (w, h) = pixmap.get_size()

        pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB,
            False, 8, w, h) 

        pixbuf = pixbuf.get_from_drawable(pixmap, pixmap.get_colormap(),
            0, 0, 0, 0, w, h)

        pixbuf.saturate_and_pixelate(pixbuf, 0, False)

        pixmap.draw_pixbuf(self.gc, pixbuf, 0, 0, 0, 0)
        """

        black_out_pixmap(pixmap, 0.5)

        context = pixmap.cairo_create()
        context.select_font_face("Arial", cairo.FONT_SLANT_NORMAL,
            cairo.FONT_WEIGHT_BOLD)
        context.set_font_size(self.psize * 1.5)

        x_bearing, y_bearing, width, height = context.text_extents(string)[:4]

        x = int((self.board_width / 2) - (width / 2))
        y = int((self.board_height / 2) - (height / 2))

        context.move_to(x, y)
        context.set_source_rgb(255, 255, 255)
        context.text_path(string)
        context.fill_preserve()

        context.set_line_width(float(self.psize) / 36)
        context.set_source_rgb(0, 0, 0)
        context.stroke()


class ScoreAnim:
    
    def __init__(self, x, maxx, psize, score):
        self.frame = 0
        self.maxframe = 15
        self.x = x*psize
        self.score = score
        self.colour = (0.9, 0.9, 0.9)
        self.shift_percent = float(x) / float(maxx)


    def animate(self):
        if self.frame == self.maxframe:
            result = False
        else:
            self.frame += 1
            result = True

        return result

    
    def render(self, pixmap, base_y, base_x):
        base_y = base_y - 30
        y = base_y - 20 - (5 * self.frame)

        alpha = 1.0 - ((1.0 / self.maxframe) * self.frame)
        colour = self.colour + (alpha,)

        draw_text("+ %d" % self.score, 20, colour, pixmap,
            base_x + self.x, y, self.shift_percent)
        
        
def draw_text(text, size, colour, pixmap, x=None, y=None, shift_percent=None):
    """Uses pango/cairo to draw the given text on the given pixmap at
    coordinates (x,y). If x or y is None, centre it in the layout."""

    (r, g, b, a) = colour

    context = pixmap.cairo_create()

    layout = context.create_layout()
    layout.set_text(text)
    
    fontdesc = pango.FontDescription("sans bold %d" % size)

    layout.set_font_description(fontdesc)

    (lwidth, lheight) = layout.get_pixel_size()
    (pwidth, pheight) = pixmap.get_size()

    if x is None:
        x = float(pwidth - lwidth) / 2

    if shift_percent is not None:
        x = x - lwidth*shift_percent
    
    if y is None:
        y = float(pheight - lheight) / 2

    context.translate(x,y)

    if a == 1.0:
        attrlist = pango.AttrList()
        r = int(r * 65535)
        g = int(g * 65535)
        b = int(b * 65535)
        attrlist.insert(pango.AttrForeground(r, g, b, 0, len(text)))
        layout.set_attributes(attrlist)
        context.show_layout(layout)
    else:
        context.set_source_rgba(*colour)
        context.layout_path(layout)
        context.fill()


def black_out_pixmap(pixmap, alpha=1):
    context = pixmap.cairo_create()
    context.set_source_rgb(0, 0, 0)
    context.rectangle(0, 0, *pixmap.get_size())
    context.paint_with_alpha(alpha)


def piece_size(piece):
    minx = min([x for (x,y) in piece._coor])
    maxx = max([x for (x,y) in piece._coor])
    miny = min([y for (x,y) in piece._coor])
    maxy = max([y for (x,y) in piece._coor])
    return (maxx-minx+1, maxy-miny+1)


